﻿Imports NAudio.Wave
Imports Q2Q.Audio.Devices
Imports Q2Q.UI

Namespace Audio.Players
	Public Class AsioPlayer
		Inherits PlayerBase
		Implements IDisposable

#Region "Properties"

		Private WithEvents AsioOutputProperty As NAudio.Wave.AsioOut
		Private Property AsioOutput As AsioOut
			Get
				Return AsioOutputProperty
			End Get
			Set(value As AsioOut)
				AsioOutputProperty = value
				RaisePropertyChanged("AsioOutput")
			End Set
		End Property

		Private SilenceProperty As SilenceWaveProvider
		Private Property Silence As SilenceWaveProvider
			Get
				Return SilenceProperty
			End Get
			Set(value As SilenceWaveProvider)
				SilenceProperty = value
				RaisePropertyChanged("Silence")
			End Set
		End Property

		Private MultiplexProperty As MultiplexingWaveProvider
		Private Property Multiplex As MultiplexingWaveProvider
			Get
				Return MultiplexProperty
			End Get
			Set(value As MultiplexingWaveProvider)
				MultiplexProperty = value
				RaisePropertyChanged("Multiplex")
			End Set
		End Property


		''' <summary>
		''' Represents the WaveController Property.
		''' </summary>
		''' <remarks></remarks>
		Private WaveControllerProperty As WaveChannel32
		''' <summary>
		''' Controls the various aspects of the playback, such as pan and volume.
		''' </summary>
		''' <value></value>
		''' <returns></returns>
		''' <remarks></remarks>
		Private Property WaveController As WaveChannel32
			Get
				Return WaveControllerProperty
			End Get
			Set(value As WaveChannel32)
				WaveControllerProperty = value
				RaisePropertyChanged("WaveController")
			End Set
		End Property

		''' <summary>
		''' Represents the Reader Property.
		''' </summary>
		''' <remarks></remarks>
		Private WithEvents ReaderProperty As LoopingAudioFileReader = Nothing	'MediaFoundationReader = Nothing
		''' <summary>
		''' The WaveStream that reads the file in for playback.
		''' </summary>
		''' <value></value>
		''' <returns></returns>
		''' <remarks></remarks>
		Private Property Reader As LoopingAudioFileReader	'MediaFoundationReader
			Get
				Return ReaderProperty
			End Get
			Set(value As LoopingAudioFileReader)
				ReaderProperty = value
				RaisePropertyChanged("Reader")
			End Set
		End Property

		''' <summary>
		''' Gets a readonly value of whether or not the audio is playing.
		''' </summary>
		''' <value></value>
		''' <returns></returns>
		''' <remarks></remarks>
		Public Overrides ReadOnly Property IsPlaying As Boolean
			Get
				If AsioOutput IsNot Nothing Then
					Return AsioOutput IsNot Nothing AndAlso AsioOutput.PlaybackState = PlaybackState.Playing
				Else
					Return False
				End If
			End Get
		End Property

		''' <summary>
		''' Gets a readonly value of whether or not the audio is stopped.
		''' </summary>
		''' <value></value>
		''' <returns></returns>
		''' <remarks></remarks>
		Public Overrides ReadOnly Property IsStopped As Boolean
			Get
				If AsioOutput IsNot Nothing Then
					Return AsioOutput IsNot Nothing AndAlso AsioOutput.PlaybackState = PlaybackState.Stopped
				Else
					Return True
				End If
			End Get
		End Property

		''' <summary>
		''' Gets a read only value of whether the audio is paused.
		''' </summary>
		''' <value></value>
		''' <returns></returns>
		''' <remarks></remarks>
		Public Overrides ReadOnly Property IsPaused As Boolean
			Get
				If AsioOutput IsNot Nothing Then
					Return AsioOutput IsNot Nothing AndAlso AsioOutput.PlaybackState = PlaybackState.Paused
				Else
					Return False
				End If
			End Get
		End Property

		''' <summary>
		''' The name of the file that was previously played. If this is different from the file currently trying to be played (because the audio cue had the file changed) then we recreate the playback stream.
		''' </summary>
		''' <value></value>
		''' <returns></returns>
		''' <remarks></remarks>
		Private Property PreviousFile As String = ""

		''' <summary>
		''' Sets the current volume of the cue.
		''' </summary>
		''' <value></value>
		''' <returns></returns>
		''' <remarks></remarks>
		Overrides Property Volume As Single
			Get
				Return VolumeProperty
			End Get
			Set(value As Single)
				If value > 1 Then
					value = 1
				ElseIf value < 0 Then
					value = 0
				End If
				VolumeProperty = value
				RaisePropertyChanged("Volume")
				If WaveController IsNot Nothing Then
					WaveController.Volume = value
					RaisePropertyChanged("ActualVolume")
				End If
			End Set
		End Property

		''' <summary>
		''' Represents the Volume Property.
		''' </summary>
		''' <remarks></remarks>
		Private VolumeProperty As Single = 0.5

		''' <summary>
		''' Returns a value that indicates the current volume of the WaveController. Returns 0 if nothing has been initialized.
		''' </summary>
		''' <value></value>
		''' <returns></returns>
		''' <remarks></remarks>
		Public ReadOnly Property ActualVolume As Single
			Get
				If WaveController IsNot Nothing Then
					Return WaveController.Volume
				Else
					Return 0
				End If
			End Get
		End Property

		''' <summary>
		''' Gets or Sets the pan value for the player. -1 is hard left. +1 is hard right.
		''' </summary>
		''' <value></value>
		''' <returns></returns>
		''' <remarks></remarks>
		Public Overrides Property Pan As Single
			Get
				Return PanProperty
			End Get
			Set(value As Single)
				If value > 1 Then
					value = 1
				ElseIf value < -1 Then
					value = -1
				End If
				PanProperty = value
				RaisePropertyChanged("Pan")
				If WaveController IsNot Nothing Then
					WaveController.Pan = value
				End If
			End Set
		End Property

		''' <summary>
		''' Represents the Pan Property.
		''' </summary>
		''' <remarks></remarks>
		Private PanProperty As Single = 0

		''' <summary>
		''' Gets or Sets whether the audio file should loop on playback.
		''' </summary>
		''' <value></value>
		''' <returns></returns>
		''' <remarks></remarks>
		Public Overrides Property EnableLoop As Boolean
			Get
				Return EnableLoopProperty
			End Get
			Set(value As Boolean)
				EnableLoopProperty = value
				If Reader IsNot Nothing Then
					Reader.EnableLooping = value
				End If
				RaisePropertyChanged("Loop")
			End Set
		End Property

		''' <summary>
		''' Represents the Loop Property.
		''' </summary>
		''' <remarks></remarks>
		Private EnableLoopProperty As Boolean = True

		''' <summary>
		''' When set to true will prevent the audio file from continuing to loop past the point it is 'stopped'.
		''' </summary>
		''' <remarks></remarks>
		Private PreventLoop As Boolean = False

		Private ChannelsListProperty As List(Of Q2Q.Audio.Devices.DeviceChannel) = Nothing
		Public Property ChannelsList As List(Of Q2Q.Audio.Devices.DeviceChannel)
			Get
				Return ChannelsListProperty
			End Get
			Set(value As List(Of Q2Q.Audio.Devices.DeviceChannel))
				ChannelsListProperty = value
				RaisePropertyChanged("ChannelsList")
			End Set
		End Property

#End Region

#Region "Public Events"
		''' <summary>
		''' Raised when the WaveOutEvent assumes the Stopped state.
		''' </summary>
		''' <remarks></remarks>
		Public Shadows Event PlaybackStopped(sender As Object, e As RoutedEventArgs)

		''' <summary>
		''' Occurs when the current time is updated.
		''' </summary>
		Public Shadows Event TimeUpdated(sender As Object, e As Helpers.TimeUpdatedEventArgs)

#End Region

#Region "Internal Functions"

		''' <summary>
		''' Raised when the WasapiOutput stops after the audio has finished playing or an error occurs.
		''' </summary>
		''' <param name="sender"></param>
		''' <param name="e"></param>
		''' <remarks></remarks>
		Private Sub WasapiOutput_PlaybackStopped(sender As Object, e As StoppedEventArgs) Handles AsioOutputProperty.PlaybackStopped
			'If e.Exception IsNot Nothing Then
			'	My.Application.Show.Console.SendMessage(e.Exception.Message)
			'End If
			PreventLoop = False
			RaisePropertyChanged("IsPlaying")
			RaisePropertyChanged("IsStopped")
			RaiseEvent PlaybackStopped(Me, New RoutedEventArgs())
			My.Application.Show.Console.SendMessage("Audio cue playback was stopped.", e.Exception)
		End Sub

		''' <summary>
		''' Handles the DoRead event of the Reader control.
		''' </summary>
		''' <param name="sender">The source of the event.</param>
		''' <param name="e">The <see cref="EventArgs"/> instance containing the event data.</param>
		Private Sub Reader_DoRead(sender As Object, e As EventArgs) Handles ReaderProperty.DoRead
			Dim tim As TimeSpan = Reader.TotalTime - Reader.CurrentTime
			If tim.TotalSeconds <= FadeOutDuration Then
				If Not IsFadingOut Then
					FadeOut()
				End If
			End If
			RaiseEvent TimeUpdated(Me, New Helpers.TimeUpdatedEventArgs(tim.Hours.ToString("D2") & ":" & tim.Minutes.ToString("D2") & ":" & tim.Seconds.ToString("D2")))
		End Sub

		''' <summary>
		''' Updates volumes based on the master volume.
		''' </summary>
		''' <param name="sender"></param>
		''' <param name="e"></param>
		''' <remarks></remarks>
		Private Sub ProcessVolumeChange(sender As Object, e As EventArgs)
			If WaveController IsNot Nothing Then
				WaveController.Volume = TargetVolume * My.Settings.MasterVolume
			End If
		End Sub

#End Region

#Region "Public Functions"

		''' <summary>
		''' Creates a new audio Player. Requires the name of the file to play.
		''' </summary>
		''' <param name="FileToPlay"></param>
		''' <remarks></remarks>
		Public Sub New(FileToPlay As String)
			AddHandler CType(My.Application.MainWindow, DesignWindow).MasterVolumeChanged, AddressOf ProcessVolumeChange
			File = FileToPlay
		End Sub

		''' <summary>
		''' Starts the file playing or resumes the player from pause.
		''' </summary>
		''' <remarks></remarks>
		Public Overrides Sub Play(Optional UseFade As Boolean = True)
			If File Is Nothing OrElse File = "" Then
				My.Application.Show.Console.SendMessage("A Cue Attempted to Play with an Invalid File or URL")
				Return
			End If
			Dim playbackState As PlaybackState = playbackState.Stopped
			If AsioOutput IsNot Nothing Then
				playbackState = AsioOutput.PlaybackState
			End If
			If playbackState <> NAudio.Wave.PlaybackState.Paused Then
				If (PreviousFile IsNot Nothing AndAlso PreviousFile <> "" AndAlso PreviousFile <> File) Then
					Reader.Dispose()
					Reader = Nothing
				End If
				If Reader Is Nothing Then
					Try
						Reader = New LoopingAudioFileReader(File)
					Catch ex As Exception
						My.Application.Show.Console.SendMessage("There was an error prepping an audio cue for playback!", ex)
						RaiseEvent PlaybackStopped(Me, New RoutedEventArgs())
						Return
					End Try
					PreviousFile = File
					If EnableLoop Then
						Reader.EnableLooping = True
					End If
					WaveController = New NAudio.Wave.WaveChannel32(Reader, TargetVolume, Pan)
					RaisePropertyChanged("ActualVolume")
					WaveController.PadWithZeroes = False
					Dim wcon As New Wave32To16Stream(WaveController)


					Silence = New SilenceWaveProvider(wcon.WaveFormat.SampleRate, wcon.WaveFormat.BitsPerSample)

					Multiplex = New MultiplexingWaveProvider(New NAudio.Wave.IWaveProvider() {wcon, Silence}, 2)

					If ChannelsList IsNot Nothing Then
						For Each d As DeviceChannel In ChannelsList
							If d.ChannelID = 0 Then
								If d.IsEnabled Then
									If My.Application.Show.ShowFile.Channel01Settings.IsEnabled Then
										Multiplex.ConnectInputToOutput(0, 0)
										Multiplex.ConnectInputToOutput(1, 1)
									Else
										Multiplex.ConnectInputToOutput(2, 0)
										Multiplex.ConnectInputToOutput(2, 1)
									End If
								Else
									Multiplex.ConnectInputToOutput(2, 0)
									Multiplex.ConnectInputToOutput(2, 1)
								End If
								'ElseIf d.ChannelID = 1 Then
								'	If d.IsEnabled Then
								'		If My.Application.Show.ShowFile.Channel23Settings.IsEnabled Then
								'			Multiplex.ConnectInputToOutput(0, 2)
								'			Multiplex.ConnectInputToOutput(1, 3)
								'		Else
								'			Multiplex.ConnectInputToOutput(2, 2)
								'			Multiplex.ConnectInputToOutput(2, 3)
								'		End If
								'	Else
								'		Multiplex.ConnectInputToOutput(2, 2)
								'		Multiplex.ConnectInputToOutput(2, 3)
								'	End If
								'ElseIf d.ChannelID = 2 Then
								'	If d.IsEnabled Then
								'		If My.Application.Show.ShowFile.Channel45Settings.IsEnabled Then
								'			Multiplex.ConnectInputToOutput(0, 4)
								'			Multiplex.ConnectInputToOutput(1, 5)
								'		Else
								'			Multiplex.ConnectInputToOutput(2, 4)
								'			Multiplex.ConnectInputToOutput(2, 5)
								'		End If
								'	Else
								'		Multiplex.ConnectInputToOutput(2, 4)
								'		Multiplex.ConnectInputToOutput(2, 5)
								'	End If
								'ElseIf d.ChannelID = 3 Then
								'	If d.IsEnabled Then
								'		If My.Application.Show.ShowFile.Channel67Settings.IsEnabled Then
								'			Multiplex.ConnectInputToOutput(0, 6)
								'			Multiplex.ConnectInputToOutput(1, 7)
								'		Else
								'			Multiplex.ConnectInputToOutput(2, 6)
								'			Multiplex.ConnectInputToOutput(2, 7)
								'		End If
								'	Else
								'		Multiplex.ConnectInputToOutput(2, 6)
								'		Multiplex.ConnectInputToOutput(2, 7)
								'	End If
							End If
						Next
					Else
						With My.Application.Show.ShowFile
							If .Channel01Settings.IsEnabled AndAlso .Channel01Settings.IsDefault Then
								Multiplex.ConnectInputToOutput(0, 0)
								Multiplex.ConnectInputToOutput(1, 1)
							Else
								Multiplex.ConnectInputToOutput(2, 0)
								Multiplex.ConnectInputToOutput(2, 1)
							End If
							'If .Channel23Settings.IsEnabled AndAlso .Channel23Settings.IsDefault Then
							'	Multiplex.ConnectInputToOutput(0, 2)
							'	Multiplex.ConnectInputToOutput(1, 3)
							'Else
							'	Multiplex.ConnectInputToOutput(2, 2)
							'	Multiplex.ConnectInputToOutput(2, 3)
							'End If
							'If .Channel45Settings.IsEnabled AndAlso .Channel45Settings.IsDefault Then
							'	Multiplex.ConnectInputToOutput(0, 4)
							'	Multiplex.ConnectInputToOutput(1, 5)
							'Else
							'	Multiplex.ConnectInputToOutput(2, 4)
							'	Multiplex.ConnectInputToOutput(2, 5)
							'End If
							'If .Channel67Settings.IsEnabled AndAlso .Channel67Settings.IsDefault Then
							'	Multiplex.ConnectInputToOutput(0, 6)
							'	Multiplex.ConnectInputToOutput(1, 7)
							'Else
							'	Multiplex.ConnectInputToOutput(2, 6)
							'	Multiplex.ConnectInputToOutput(2, 7)
							'End If
						End With
					End If



					Try
						If FadeInDuration > 0 AndAlso UseFade Then
							WaveController.Volume = 0
							RaisePropertyChanged("ActualVolume")
							AsioOutput.Init(Multiplex)
							FadeIn()
						Else
							WaveController.Volume = TargetVolume * My.Settings.MasterVolume
							AsioOutput.Init(Multiplex)
						End If
					Catch ex As Exception
						My.Application.Show.Console.SendMessage("There was a problem initializing audio playback!", ex)
					End Try
				End If
			End If

			WaveController.CurrentTime = StartTime

			AsioOutput.Play()
			RaisePropertyChanged("IsPlaying")
			RaisePropertyChanged("IsStopped")
			RaisePropertyChanged("IsPaused")
			My.Application.Show.Console.SendMessage("Audio cue playback was started.")
		End Sub

		''' <summary>
		''' Stops the audio player.
		''' </summary>
		''' <remarks></remarks>
		Public Overrides Sub [Stop](Optional UseFade As Boolean = True)
			If Not IsStopped Then
				PreventLoop = True
				'If AsioOutput IsNot Nothing Then
				'	If FadeOutDuration > 0 AndAlso UseFade Then
				'		FadeOut()
				'	Else
				'		FakeStop()
				'	End If
				'End If
				If AsioOutput IsNot Nothing Then
					If FadeOutDuration > 0 AndAlso UseFade Then
						FadeOut()
					Else
						AsioOutput.Stop()
						RaisePropertyChanged("IsPlaying")
						RaisePropertyChanged("IsStopped")
						RaisePropertyChanged("IsPaused")
					End If
				End If
			End If
		End Sub

		''' <summary>
		''' Sets the volume of the cue playback to zero, instantly.
		''' </summary>
		''' <remarks></remarks>
		Public Overrides Sub Mute()
			WaveController.Volume = 0
		End Sub

		'Public Sub FakeStop()
		'	Silence.Stop()
		'	RaiseEvent PlaybackStopped(Me, New RoutedEventArgs)
		'End Sub

		''' <summary>
		''' Pauses the audio playback.
		''' </summary>
		''' <remarks></remarks>
		Public Overrides Sub Pause()
			If AsioOutput IsNot Nothing Then
				AsioOutput.Pause()
				RaisePropertyChanged("IsPlaying")
				RaisePropertyChanged("IsStopped")
				RaisePropertyChanged("IsPaused")
			End If

		End Sub

		''' <summary>
		''' Dispose of the internal pieces of the player when ready to remove it from memory
		''' </summary>
		''' <remarks></remarks>
		Public Overrides Sub Dispose() Implements IDisposable.Dispose
			Dispose(True)
			GC.SuppressFinalize(Me)
		End Sub

		''' <summary>
		''' Handles the actual disposing of the interal parts of the player.
		''' </summary>
		''' <param name="disposing"></param>
		''' <remarks></remarks>
		Protected Overridable Overloads Sub Dispose(disposing As Boolean)
			RemoveHandler CType(My.Application.MainWindow, DesignWindow).MasterVolumeChanged, AddressOf ProcessVolumeChange
			If disposing Then
				If WaveController IsNot Nothing Then
					Try
						WaveController.Dispose()
					Catch ex As Exception
						My.Application.Show.Console.SendMessage("There was a problem releasing an audio cue's resources. This may cause memory problems.", ex)
					End Try
					WaveController = Nothing
				End If
				If AsioOutput IsNot Nothing Then
					AsioOutput.Dispose()
					AsioOutput = Nothing
					Silence = Nothing
					Multiplex = Nothing
					RaisePropertyChanged("IsPlaying")
					RaisePropertyChanged("IsStopped")
					RaisePropertyChanged("IsPaused")
				End If
				If Reader IsNot Nothing Then
					Reader.Dispose()
					Reader = Nothing
				End If
			End If
		End Sub

#End Region

#Region "Fading"

		''' <summary>
		''' The amount the sound should be stepped by when fading in.
		''' </summary>
		''' <remarks></remarks>
		Private FadeInStep As Double

		''' <summary>
		''' The amount the sound should be stepped by when fading out.
		''' </summary>
		''' <remarks></remarks>
		Private FadeOutStep As Double



		''' <summary>
		''' Fade in the audio after the player is playing.
		''' </summary>
		''' <remarks></remarks>
		Private Sub FadeIn()
			If IsFadingIn Then
				Return
			End If
			FadeInStep = ((TargetVolume * My.Settings.MasterVolume) / FadeInDuration) / 10
			Dim t As New Timers.Timer(100)
			AddHandler t.Elapsed, AddressOf DoFadeInStep
			IsFadingIn = True
			t.Start()
		End Sub

		''' <summary>
		''' Returns True when the player is fading in audio.
		''' </summary>
		''' <remarks></remarks>
		Private IsFadingIn As Boolean = False

		''' <summary>
		''' Called by the timer created in the FadeIn sub to handle raising the volume by the prescribed amount.
		''' </summary>
		''' <param name="sender"></param>
		''' <param name="e"></param>
		''' <remarks></remarks>
		Private Sub DoFadeInStep(sender As Object, e As System.Timers.ElapsedEventArgs)
			If WaveController IsNot Nothing Then
				If Not IsFadingIn Then
					CType(sender, System.Timers.Timer).Stop()
					RemoveHandler CType(sender, System.Timers.Timer).Elapsed, AddressOf DoFadeInStep
					Return
				End If
				WaveController.Volume += FadeInStep
				RaisePropertyChanged("ActualVolume")
				If WaveController.Volume >= TargetVolume Then
					CType(sender, System.Timers.Timer).Stop()
					RemoveHandler CType(sender, System.Timers.Timer).Elapsed, AddressOf DoFadeInStep
				End If
			End If
		End Sub


		''' <summary>
		''' Returns true if the player is fading out audio.
		''' </summary>
		Private IsFadingOut As Boolean = False

		''' <summary>
		''' Fade out the audio and then stop the player.
		''' </summary>
		''' <remarks></remarks>
		Private Sub FadeOut()
			If IsFadingOut Then
				Return
			End If
			'Determine the time remaining for the audio cue that's playing back.
			Dim remainder As Integer = (WaveController.TotalTime - WaveController.CurrentTime).TotalSeconds

			'If the time remaining is greater than the requested fade out duration then use the fade out duration, otherwise fade out based on the reamining time.
			If FadeOutDuration < remainder Then
				FadeOutStep = ((WaveController.Volume * My.Settings.MasterVolume) / FadeOutDuration) / 10
			Else
				FadeOutStep = ((WaveController.Volume * My.Settings.MasterVolume) / remainder) / 10
			End If
			Dim t As New Timers.Timer(100)
			AddHandler t.Elapsed, AddressOf DoFadeOutStep
			IsFadingIn = False
			IsFadingOut = True
			t.Start()
		End Sub

		''' <summary>
		''' Called by the timer created in the FadeOut sub to handle lowering the volume by the prescribed amount.
		''' </summary>
		''' <param name="sender"></param>
		''' <param name="e"></param>
		''' <remarks></remarks>
		Private Sub DoFadeOutStep(sender As Object, e As System.Timers.ElapsedEventArgs)
			If WaveController IsNot Nothing Then
				If IsStopped Then
					CType(sender, System.Timers.Timer).Stop()
					RemoveHandler CType(sender, System.Timers.Timer).Elapsed, AddressOf DoFadeOutStep
					IsFadingOut = False
					Return
				End If
				WaveController.Volume -= FadeOutStep
				RaisePropertyChanged("ActualVolume")
				If WaveController.Volume <= 0 Then
					CType(sender, System.Timers.Timer).Stop()
					RemoveHandler CType(sender, System.Timers.Timer).Elapsed, AddressOf DoFadeOutStep
					If Reader.EnableLooping AndAlso PreventLoop = False Then
						If FadeInDuration > 0 Then
							FadeIn()
						Else
							WaveController.Volume = TargetVolume
							RaisePropertyChanged("ActualVolume")
						End If
					Else
						'FakeStop()
						AsioOutput.Stop()
						RaisePropertyChanged("IsPlaying")
						RaisePropertyChanged("IsStopped")
						RaisePropertyChanged("IsPaused")
					End If
				End If
			End If
		End Sub

#End Region
	End Class
End Namespace
